Skip to content

fix: fail cancelled smart transactions through the standard failure path#9400

Merged
cloudonshore merged 3 commits into
mainfrom
fix/stx-cancelled-tx-missing-status-event
Jul 7, 2026
Merged

fix: fail cancelled smart transactions through the standard failure path#9400
cloudonshore merged 3 commits into
mainfrom
fix/stx-cancelled-tx-missing-status-event

Conversation

@cloudonshore

@cloudonshore cloudonshore commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Explanation

When the transaction relay cancels a smart transaction (e.g. FAILED_GAS_TOO_LOW — the smart tx never lands on chain), SmartTransactionsController marks the associated regular transaction as failed via the TransactionController:updateTransaction action.

updateTransaction only patches state — it does not emit any transaction lifecycle events. As a result, consumers that react to transactionFailed / transactionStatusUpdated are never notified:

  • BridgeStatusController marks a bridge item failed only from the transactionStatusUpdated / transactionFailed event. Since that event never fires for a cancelled smart tx, the bridge history item stays PENDING forever — the transaction shows as failed, but the bridge UI remains stuck on "Pending".
  • Transaction metrics (Transaction Finalized) are likely under-reported for the same reason.

This was observed in production with bridge smart transactions repeatedly stuck as pending (source tx status: failed, bridge status.status: PENDING), while the relay's batchStatus reported minedTx: cancelled / cancellationReason: too_cheap.

Fix

Route the cancellation failure through the standard fail path so the lifecycle events fire.

  • transaction-controller: add a public failTransaction(transactionId, error) method and corresponding TransactionController:failTransaction messenger action. It fails the transaction through the internal #failTransaction, emitting transactionFailed, transactionStatusUpdated, and transactionFinished.
  • smart-transactions-controller: markRegularTransactionsAsFailed now calls failTransaction instead of updateTransaction.

References

  • Fixes the "smart transaction bridge stuck as pending" reports.

Changelog

@metamask/transaction-controller

  • ADDED: failTransaction method and TransactionController:failTransaction messenger action.

@metamask/smart-transactions-controller

  • FIXED: Fail the associated regular transaction via TransactionController:failTransaction (was updateTransaction) so transaction lifecycle events are emitted and downstream consumers no longer leave cancelled smart transactions stuck as pending.
    • Consumers must grant the smart-transactions-controller messenger access to TransactionController:failTransaction (previously TransactionController:updateTransaction).

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc) for new or updated code as appropriate
  • I've highlighted breaking changes using the "BREAKING" category above as appropriate

Note

Medium Risk
Touches core transaction lifecycle and messenger permissions for smart-transactions; behavior change is intentional but clients must update delegated actions or cancellation failures will not propagate.

Overview
When a smart transaction is cancelled, the linked regular transaction is now failed through TransactionController:failTransaction instead of updateTransaction, so transactionFailed, transactionStatusUpdated, and transactionFinished are emitted. That fixes cases where the tx looked failed but bridge history and similar subscribers stayed pending because they only react to those events.

@metamask/transaction-controller exposes public failTransaction(transactionId, error) and the TransactionController:failTransaction messenger action, delegating to the existing internal failure path. @metamask/smart-transactions-controller updates markRegularTransactionsAsFailed and messenger allowances to call it with a SmartTransactionFailed error (same matching rules for tx id / hashes; still skips already-failed txs).

Integrators: grant TransactionController:failTransaction to the smart transactions controller messenger (replacing TransactionController:updateTransaction for this flow).

Reviewed by Cursor Bugbot for commit 1cdd855. Bugbot is set up for automated code reviews on this repo. Configure here.

When the relay cancels a smart transaction (e.g. FAILED_GAS_TOO_LOW), the
smart-transactions-controller marked the associated regular transaction as
failed via `TransactionController:updateTransaction`, which only patches state
and does not emit any transaction lifecycle events. Consumers that react to
`transactionFailed`/`transactionStatusUpdated` — notably the bridge status
controller and transaction metrics — were therefore never notified, leaving
cancelled smart transactions (such as bridges) stuck as pending indefinitely.

- transaction-controller: add a public `failTransaction(transactionId, error)`
  method and `TransactionController:failTransaction` messenger action that fails
  a transaction through the internal fail path, emitting `transactionFailed`,
  `transactionStatusUpdated`, and `transactionFinished`.
- smart-transactions-controller: use `failTransaction` instead of
  `updateTransaction` when marking regular transactions as failed.
@cloudonshore cloudonshore requested review from a team as code owners July 7, 2026 01:31
@matthewwalsh0 matthewwalsh0 self-requested a review July 7, 2026 11:00
* @param transactionId - The ID of the transaction to mark as failed.
* @param error - The error describing why the transaction failed.
*/
failTransaction(transactionId: string, error: Error): void {

@matthewwalsh0 matthewwalsh0 Jul 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will ultimately generate the same state as the previous update, except the explicit calls to #onTransactionStatusChange means the status change events are a conscious decision rather than derived from the state which should be the source of truth.

Could we avoid any changes to the SmartTransactionController and capture all status changes entirely automatically, if we instead scrap the calls to #onTransactionStatusChange and instead diff the previous and new status inside #updateTransactionInternal and throw the event(s) there after the update call?

Or Confirmations team could do that in a follow up to minimise effort here if time-sensitive.

@matthewwalsh0 matthewwalsh0 Jul 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the cleanest approach but it has more overhead given the additional events too, will come back to this internally later given the risk.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed that's the cleaner long-term shape. Deriving the events from the status diff in #updateTransactionInternal would kill off this whole class of "caller forgot to emit" bugs and let us drop the manual #onTransactionStatusChange calls plus this failTransaction wrapper.

I kept it narrow here mainly because it's not just transactionStatusUpdated. Each transition also fires transactionFailed/transactionConfirmed/transactionDropped/transactionFinished, so doing it right means deriving all of those from the diff and pulling out the ~8 manual call sites so we don't double-fire. Felt like too much surface area to fold into a stuck-bridge fix. This version just goes through the existing #failTransaction, so the end state is identical (like you said) and it matches the other failure paths.

I was hoping we could get in this targeted fix now. I opened an issue for the follow-up refactor so Confirmations can pick it up when there's bandwidth: #9412


### Fixed

- Fail the associated regular transaction via the new `TransactionController:failTransaction` action instead of `TransactionController:updateTransaction` when a smart transaction is cancelled ([#9400](https://github.com/MetaMask/core/pull/9400))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a breaking change ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cloudonshore cloudonshore added this pull request to the merge queue Jul 7, 2026
Merged via the queue into main with commit c881134 Jul 7, 2026
417 checks passed
@cloudonshore cloudonshore deleted the fix/stx-cancelled-tx-missing-status-event branch July 7, 2026 19:53
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Jul 7, 2026
## Explanation

Follow-up to MetaMask#9400 (already merged). That PR changed the
`smart-transactions-controller` messenger requirement from
`TransactionController:updateTransaction` to
`TransactionController:failTransaction`. Adding/removing a required
allowed action is a **breaking change** for consumers — they must
delegate the new action to the controller messenger on the client side
(extension/mobile) or they get a type error.

The changelog entry was filed under `### Fixed`, which would have the
package cut as a patch release and silently break consumers on upgrade.
This recategorizes it under `### Changed` with a `**BREAKING:**` marker
so the next release is correctly a **major** version bump (`24.2.4` →
`25.0.0`), consistent with how allowed-action changes are documented
elsewhere in the monorepo (e.g. `AssetsControllerMessenger now requires
...` in assets-controller, `NftController allowed actions` in
assets-controllers).

No code changes — changelog only.

## References

- Follow-up to MetaMask#9400
- Raised in review by @cryptodev-2s

## Changelog

### `@metamask/smart-transactions-controller`

- Recategorized the MetaMask#9400 entry from `Fixed` to `Changed` and marked it
`BREAKING`; no functional change.

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
(N/A — changelog only)
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate (changelog)
- [x] I've highlighted breaking changes using the 'BREAKING' category
above as appropriate

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changelog edit with no code, tests, or runtime
behavior changes.
> 
> **Overview**
> Updates `@metamask/smart-transactions-controller` **Unreleased**
changelog only: the MetaMask#9400 note about using
`TransactionController:failTransaction` instead of
`TransactionController:updateTransaction` when a smart transaction is
cancelled moves from `### Fixed` to `### Changed` and is labeled
**`BREAKING:`**, with explicit guidance that consumers must delegate the
new allowed action.
> 
> The separate `### Fixed` section is removed and the
`@metamask/messenger` bump stays under `### Changed`. **No runtime or
API code changes** in this PR—only semver/documentation so the next
release is treated as major for the messenger requirement change from
MetaMask#9400.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
3bdac95. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
pull Bot pushed a commit to dmrazzy/core that referenced this pull request Jul 7, 2026
## Explanation

Releases two packages needed to fix stuck/underpriced bridge smart
transactions in the clients (extension + mobile).

### `@metamask/transaction-controller` — `68.2.2` → `68.3.0` (minor)
- **Added:** `failTransaction` method +
`TransactionController:failTransaction` messenger action
([MetaMask#9400](MetaMask#9400)) — fails a tx
through the standard path, emitting
`transactionFailed`/`transactionStatusUpdated`/`transactionFinished`.
- **Fixed:** Only apply user-saved (advanced) gas fees to dApp
transactions; ignored for internal (`isInternal`) txs such as swaps and
bridges ([MetaMask#9401](MetaMask#9401)).
- **Changed:** Bump `@metamask/messenger` `^1.2.0` → `^2.0.0`
([MetaMask#9392](MetaMask#9392)).

### `@metamask/smart-transactions-controller` — `24.2.4` → `25.0.0`
(major)
- **Changed (BREAKING):** Fail the associated regular transaction via
`TransactionController:failTransaction` instead of `updateTransaction`
when a smart transaction is cancelled
([MetaMask#9400](MetaMask#9400)). Consumers must
now grant the STX messenger access to
`TransactionController:failTransaction`.
- **Changed:** Bump `@metamask/messenger` `^1.2.0` → `^2.0.0`
([MetaMask#9392](MetaMask#9392)).

All other changed packages are `intentionally-skip` (owned by other
teams); dependent version ranges were updated automatically.

## References

- Fixes stuck-pending bridge STX (root cause in MetaMask#9400) and underpriced
bridge gas (MetaMask#9401).

## Checklist

- [x] I've followed the [release
process](https://github.com/MetaMask/core/blob/main/docs/processes/releasing.md)
- [x] Changelogs reviewed and categorized (no `Uncategorized`; breaking
change marked)
- [x] Version bumps follow SemVer (STX major for the breaking messenger
change)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches transaction failure signaling and gas on internal bridge
flows; STX **25.0.0** is a breaking messenger permission change for
extension/mobile integrators.
> 
> **Overview**
> **Monorepo release `1102.0.0`** that publishes
**`@metamask/transaction-controller` `68.3.0`** and
**`@metamask/smart-transactions-controller` `25.0.0`**, and rolls
**`@metamask/transaction-controller` `^68.2.2` → `^68.3.0`** through
dependent packages and **`yarn.lock`**.
> 
> **`transaction-controller` `68.3.0`** (documented in its changelog for
this release): adds **`failTransaction`** /
**`TransactionController:failTransaction`** so out-of-band failures emit
normal lifecycle events; fixes **advanced gas fees** so they apply only
to dApp txs, not **`isInternal`** bridge/swap txs.
> 
> **`smart-transactions-controller` `25.0.0` (breaking):** on smart-tx
cancel, calls **`failTransaction`** instead of **`updateTransaction`**;
hosts must delegate **`TransactionController:failTransaction`** on the
STX messenger.
> 
> Other touched packages in the diff are mainly **version/changelog
dependency bumps** tied to this release, not new feature code in those
files.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
c423a08. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants